<template>
  <div
    class="layout_one"
    v-loading="loading"
    :element-loading-text="loadingText"
    element-loading-spinner="el-icon-loading"
    element-loading-background="rgba(0, 0, 0, 0.8)"
  >
    <div class="shade" v-if="!isJoin">
      <div class="input-container" v-if="isConnect">
        <el-input
          v-model="account"
          placeholder="请输入你的昵称"
          @keyup.enter="join"
        >
          <template #append>
            <el-button @click="join" type="primary">确定</el-button>
          </template>
        </el-input>
      </div>
      <el-result :icon="icon" :title="title" :sub-title="subTitle" v-else />
    </div>
    <el-container class="container" v-else>
      <el-aside class="aside">
        <svg-icon
          :name="`connectlogo`"
          :myStyle="{ fontSize: '80px' }"
        ></svg-icon>
        <left-icon
          :name="`tuichu`"
          :myStyle="{ fontSize: `40px` }"
          :title="`退出`"
          @click="leave"
          style="transform: translate(0, calc(100vh - 260px))"
        />
      </el-aside>
      <el-aside width="240px" style="border-right: solid 1px #cdd0d6">
        <el-header height="60px"> 在线用户：{{ userList.length }} </el-header>
        <el-scrollbar height="calc(100vh - 160px)">
          <el-main class="main">
            <div class="userList" v-for="v in userList" :key="v.account">
              {{ v.account }}
              <i v-if="v.account === account || v.account === isCall">
                {{ v.account === account ? "me" : "" }}
                {{ v.account === isCall ? "calling" : "" }}
              </i>
              <div
                @click="apply(v.account)"
                v-if="v.account !== account && v.account !== isCall"
              >
                呼叫
              </div>
            </div>
          </el-main>
        </el-scrollbar>
      </el-aside>
      <el-container v-show="isCall">
        <el-header height="60px"> 与 {{ isCall }} 正在通话中 </el-header>
        <el-main>
          <video
            ref="videoRefB"
            controls
            autoplay
            playsinline
            style="height: 99%; width: 100%"
          />
          <video
            ref="videoRefA"
            controls
            autoplay
            playsinline
            width="400"
            height="300"
            style="position: fixed; top: 50%; left: 77%; border: solid 1px red"
          />
        </el-main>
        <el-footer>
          <el-button size="large" type="danger" @click="hangup">
            结束通话
          </el-button>
        </el-footer>
      </el-container>
    </el-container>
  </div>
</template>
        
<script lang="ts">
import {
  defineComponent,
  onBeforeMount,
  onBeforeUnmount,
  onMounted,
  reactive,
  toRefs,
} from "vue";
import leftIcon from "@/components/aside/leftIcon.vue";
import { ElMessage, ElMessageBox } from "element-plus";
import { io } from "socket.io-client";

export default defineComponent({
  components: { leftIcon },
  name: "RemoteConnection",
  setup() {
    const state = reactive({
      isConnect: null as any, // 是否连接到服务器
      icon: "info" as string,
      title: "建立连接" as string,
      subTitle: "正在与服务器建立连接......" as string,
      account: window.sessionStorage.account || ("" as string), // 账号
      isJoin: false as boolean, // 是否已加入
      userList: [] as Array<any>, // 用户列表
      roomId: "webrtc_1v1", // 指定房间ID
      applyTimeOut: 5 as number, // 视频邀请超时时间，单位：秒
      isTimeOut: false as boolean, // 是否超时
      timeout: null as any, // 计时器
      loading: false as boolean,
      loadingText: "呼叫中" as string,
      isCall: false as boolean, // 正在通话的对象
      peer: null as RTCPeerConnection | null,
      isToPeer: false, // 是否建立了 P2P 连接
      localStream: null as MediaStream | null,
      videoRefA: {} as any,
      videoRefB: {} as any,
      offerOption: {
        offerToReceiveAudio: true,
        offerToReceiveVideo: true,
      },
      socket: io("localhost:3001", { reconnection: false, autoConnect: false }),
      activeHangUp: false as boolean, // 主动断开
    });

    /**
     * 该方法用于连接socket服务器，并
     * 1、处理连接成功事件
     * 2、处理连接失败事件
     * 3、处理断开连接事件
     */
    const connect = () => {
      state.socket.io.open((err) => {
        if (err) {
          ElMessage({
            message: "连接服务器失败，请联系管理员！",
            type: "error",
          });
          state.icon = "error";
          state.title = "连接失败";
          state.subTitle = "连接服务器失败，请联系管理员！";
          state.isConnect = false;
        } else {
          ElMessage({
            message: "成功连接服务器！",
            type: "success",
          });
          state.isConnect = true;
        }
      });
      state.socket.on("disconnect", () => {
        ElMessage({
          message: "已断开连接！",
          type: "success",
        });
        sessionStorage.removeItem("account");
        state.userList = [];
        state.account = "";
        state.isJoin = false;
      });
    };

    /**
     * 加入房间
     */
    const join = () => {
      if (!state.account) return;
      state.isJoin = true;
      sessionStorage.account = state.account;
      state.socket.connect();
      state.socket.emit("join", {
        roomId: state.roomId,
        account: state.account,
      });
    };

    // 初始化socket
    const initSocket = async () => {
      /**
       * 用户加入房间
       */
      state.socket.on("joined", (data) => {
        state.userList = data;
      });
      /**
       * 收到视频邀请
       */
      state.socket.on("apply", (data) => {
        state.loading = false;
        // 正在通话中
        if (state.isCall) {
          reply(data.self, "3");
          return;
        }
        ElMessageBox.confirm(
          data.self + " 向你请求视频通话，是否同意?",
          "提示",
          {
            confirmButtonText: "同意",
            cancelButtonText: "拒绝",
            type: "warning",
          }
        )
          .then(async () => {
            clearTimeout(state.timeout);
            if (!state.isTimeOut) {
              // 同意之后创建自己的 peer 等待对方的 offer
              const flag = await createP2P(data);
              if (flag) {
                reply(data.self, "1");
              }
            } else handleTimeout(); // 答复超时
          })
          .catch(() => {
            clearTimeout(state.timeout);
            if (!state.isTimeOut) {
              reply(data.self, "2");
            } else handleTimeout(); // 答复超时
          });
        handleReply(data); // 答复是否超时
      });
      /**
       * 收到回复
       */
      state.socket.on("reply", async (data) => {
        switch (data.type) {
          // 同意
          case "1":
            // 对方同意之后创建自己的 peer
            const flag = await createP2P(data);
            if (flag) {
              state.isCall = data.self;
              // 并给对方发送 offer
              createOffer(data);
            }
            break;
          // 拒绝
          case "2":
            ElMessage({
              message: "对方已拒绝！",
              type: "warning",
            });
            state.loading = false;
            break;
          // 正在通话中
          case "3":
            ElMessage({
              message: "对方正在通话中!",
              type: "warning",
            });
            state.loading = false;
            break;
          // 超时未答复
          case "4":
            ElMessage({
              message: "对方未答复！",
              type: "warning",
            });
            state.loading = false;
            handleHangUp();
            break;
          default:
            break;
        }
      });

      // 接收到answer
      state.socket.on("1v1answer", async (data) => {
        await onAnswer(data);
      });
      // 接收到 ICE
      state.socket.on("1v1ICE", async (data) => {
        await onIce(data);
      });
      // 接收到 offer
      state.socket.on("1v1offer", async (data) => {
        state.isCall = data.self;
        await onOffer(data);
      });
      // 通话挂断
      state.socket.on("1v1hangup", () => {
        ElMessage({ message: "对方已挂断通话！", type: "warning" });
        handleHangUp();
      });
      // 断开连接
      state.socket.on("leaved", (data) => {
        state.userList = data;
      });
    };

    // 挂断通话
    const hangup = () => {
      state.socket.emit("1v1hangup", {
        account: state.isCall,
        self: state.account,
      });
      handleHangUp();
    };

    const handleHangUp = () => {
      state.activeHangUp = true;
      if (state.peer) {
        state.peer.close();
        state.peer = null;
      }
      state.isToPeer = false;
      state.isCall = false;
      if (state.localStream) {
        state.localStream.getTracks().forEach((track) => track.stop());
      }
    };

    // 呼叫
    const apply = (account: string) => {
      state.loading = true;
      state.loadingText = "呼叫中";
      state.socket.emit("apply", { account: account, self: state.account });
    };

    // 答复
    const reply = (account: string, type: string) => {
      state.socket.emit("reply", {
        account: account,
        self: state.account,
        type: type,
      });
    };

    // 断开连接
    const leave = () => {
      state.socket.emit("leave", {
        roomId: state.roomId,
        account: state.account,
      });
      state.socket.close();
    };

    // 建立连接
    const createP2P = async (data: any) => {
      state.loading = true;
      state.loadingText = "正在建立通话连接......";
      return createMedia(data);
    };

    // 保存本地流到全局
    const createMedia = (data: any) => {
      try {
        handleReply(data);
        return navigator.mediaDevices
          .getDisplayMedia({
            audio: true,
            video: true,
          })
          .then((stream) => {
            clearTimeout(state.timeout);
            if (!state.isTimeOut) {
              addStreamStopListener(stream);
              state.localStream = stream;
              state.videoRefA.srcObject = state.localStream;
              // 获取到媒体流后，调用函数初始化 RTCPeerConnection
              initPeer(data);
              return true;
            } else {
              stream.getTracks().forEach((track) => track.stop());
              state.loading = false;
              handleTimeout(); //答复超时
              return false;
            }
          })
          .catch((error) => {
            clearTimeout(state.timeout);
            state.loading = false;
            if (!state.isTimeOut) {
              reply(data.self, "2");
            } else {
              handleTimeout(); //答复超时
            }
            return false;
          });
      } catch (error) {
        console.log("getDisplayMedia", error);
      }
    };

    const initPeer = (data: any) => {
      // 创建输出端 PeerConnection
      state.peer = new RTCPeerConnection();
      if (state.localStream) {
        // 添加本地流
        state.localStream.getTracks().forEach((tarck) => {
          if (state.peer && state.localStream) {
            state.peer.addTrack(tarck, state.localStream);
          }
        });
      }
      // 监听是否有媒体流接入，如果有就赋值给 videoRefB 的 src
      state.peer.ontrack = (event) => {
        state.isToPeer = true;
        state.videoRefB.srcObject = event.streams[0];
      };
      // 监听ICE候选信息 如果收集到，就发送给对方
      state.peer.onicecandidate = (event) => {
        if (event.candidate) {
          state.socket.emit("1v1ICE", {
            account: data.self,
            self: state.account,
            sdp: event.candidate,
          });
        }
      };
    };

    // 创建并发送 offer
    const createOffer = async (data: any) => {
      try {
        if (state.peer) {
          state.loading = false;
          // 创建offer
          const offer = await state.peer.createOffer(state.offerOption);
          // 呼叫端设置本地 offer 描述
          await state.peer.setLocalDescription(offer);
          // 给对方发送 offer
          state.socket.emit("1v1offer", {
            account: data.self,
            self: state.account,
            sdp: offer,
          });
        }
      } catch (error) {
        console.log("createOffer", error);
      }
    };

    // 接收offer并发送 answer
    const onOffer = async (data: any) => {
      try {
        if (state.peer) {
          state.loading = false;
          // 接收端设置远程 offer 描述
          await state.peer.setRemoteDescription(data.sdp);
          // 接收端创建 answer
          const answer = await state.peer.createAnswer();
          // 接收端设置本地 answer 描述
          await state.peer.setLocalDescription(answer);
          // 给对方发送 answer
          state.socket.emit("1v1answer", {
            account: data.self,
            self: state.account,
            sdp: answer,
          });
        }
      } catch (error) {
        console.log("onOffer: ", error);
      }
    };

    // 接收answer
    const onAnswer = async (data: any) => {
      try {
        if (state.peer) {
          // 呼叫端设置远程 answer 描述
          await state.peer.setRemoteDescription(data.sdp);
        }
      } catch (error) {
        console.log("onAnswer: ", error);
      }
    };

    // 接收 ICE 候选
    const onIce = async (data: any) => {
      try {
        if (state.peer) {
          // 设置远程 ICE
          await state.peer.addIceCandidate(data.sdp);
        }
      } catch (error) {
        console.log("onIce: ", error);
      }
    };

    // 停止录制
    const addStreamStopListener = (stream: any) => {
      stream.addEventListener(
        "inactive",
        () => {
          if (!state.activeHangUp) {
            console.log("inactive");
            state.socket.emit("1v1hangup", {
              account: state.isCall,
              self: state.account,
            });
            state.peer = null;
            state.isToPeer = false;
            state.isCall = false;
          }
        },
        false
      );
      stream.getTracks().forEach((track: any) => {
        track.addEventListener(
          "ended",
          () => {
            console.log("track ended");
            state.activeHangUp = false;
          },
          false
        );
      });
    };

    /**
     * 处理超时答复，到达设置超时时间后，自动发送答复超时通知
     * @param data
     */
    const handleReply = (data: any) => {
      state.timeout = setTimeout(() => {
        state.isTimeOut = true;
        reply(data.self, "4");
      }, state.applyTimeOut * 1000);
    };

    /**
     * 答复超时，提示超时信息
     */
    const handleTimeout = () => {
      state.isTimeOut = false;
      ElMessage({
        message: "答复超时，已断开！",
        type: "warning",
      });
    };

    const beforeunload = () => {
      if (state.isCall) {
        // 挂断通话
        hangup();
      }
      // 断开连接
      leave();
    };

    onBeforeMount(() => {
      window.addEventListener("beforeunload", (e) => beforeunload());
    });

    onBeforeUnmount(() => {
      window.removeEventListener("beforeunload", (e) => beforeunload());
    });

    onMounted(() => {
      connect();
      initSocket();
      if (state.account) {
        join();
      }
    });

    return {
      ...toRefs(state),
      join,
      apply,
      hangup,
      leave,
    };
  },
});
</script>
        
<style lang="scss" scoped>
.layout_one {
  background: #fff;
  height: calc(100vh - 100px);
  box-shadow: 0 0 3px #e4e7ed;
  border-radius: 10px;

  .aside {
    background: #000;
    width: 80px;
    border-top-left-radius: 10px;
    border-bottom-left-radius: 10px;
  }
}
.layout_one:hover {
  box-shadow: 0 0 5px #cdd0d6;
}
.shade {
  position: absolute;
  width: 100%;
  height: 100%;
  left: 0;
  top: 0;
  z-index: 100;
  background-color: rgba(0, 0, 0, 0.9);
  .input-container {
    position: absolute;
    left: 50%;
    top: 30%;
    transform: translate(-50%, 50%);
    display: flex;
    justify-content: space-between;
    align-items: center;
    input {
      margin: 0;
    }
  }
  .el-result {
    position: absolute;
    left: 50%;
    top: 20%;
    transform: translate(-50%, 50%);
    background-color: #fff;
    border-radius: 10px;
  }
}
.el-header {
  line-height: 60px;
  text-align: left;
}
.userList {
  border: 1px solid #ddd;
  height: 50px;
  width: 100%;
  background: #cdd0d6;
  line-height: 50px;
  padding-left: 10px;

  div {
    position: relative;
    left: 100%;
    top: -100%;
    background-color: #55ca1f;
    color: #fff;
    height: 100%;
    transition: left 0.2s;
    width: 100%;
    padding-left: 10px;
    cursor: pointer;
  }

  i {
    font-style: normal;
    font-size: 11px;
    border: 1px solid #1fbeca;
    color: #27cac7;
    border-radius: 2px;
    line-height: 1;
    padding: 1px 2px;
  }
}
.userList:hover div {
  left: 70%;
}
video {
  background-color: #ddd;
}

.el-container {
  height: 100%;
}
.el-aside {
  overflow: hidden;
}
.main {
  background: #cdd0d6;
  padding: 0px;
  overflow: hidden;
}
.el-footer {
  text-align: center;
}
</style>